iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 12
2
自我挑戰組

寫遊戲初體驗系列 第 12

Day 12 [OpenGL] Textures

  • 分享至 

  • xImage
  •  

Texture

紋理(Texture)是一個 2D 的圖片,可以用來增添物體的細節,可以想像 Texture 就像是個有圖案的紙,貼合在 3D 的物體上,這樣就可以讓物體不用增加頂點就增加細節。
題外話,Texture 除了圖像之外還能用來儲存大量資訊發送到 Shader 上。

上個文章中的三角形貼上一個磚頭的 Texture:

為了將紋理(Texture)映射(Map)到頂點上,需要指定頂點分邊對應到紋理的哪個部分,所以每個頂點都會關聯著一個紋理座標(Texture Coordinate),用來表示該從 Texture 的哪個地方採樣(Sampling)(fragment 的顏色),之後其他的 Fragment 會插值其 Texture Coordinate。

我們希望三角形的左下角對應 Texture 的左下角,因此我們將左下角頂點的 Texture 座標設置為 $(0,0)$ 上方設置為 $(0.5, 1)$ 右下設置為 $(1, 0)$。只要給定這三個座標就可以了,接下來他們會被傳入到 Fragment Shader 中進行處裡。

float texCoords[] = {
    0.0f, 0.0f, // bottom left
    1.0f, 0.0f, // bottom right
    0.5f, 1.0f  // middle
};
float vertices[] = {
    // 位置  
     0.5f, -0.5f, 0.0f,
    -0.5f, -0.5f, 0.0f,
     0.0f,  0.5f, 0.0f
};

Texture 環繞方式

座標的範圍是$[0, 1]$(第一象限),如果超出這個座標之外預設的行為是重複,但其實 OpenGL 提供了更多的選擇:

  • GL_REPEAT
    • 預設,重複材質圖像
  • GL_MIRRORED_REPEAT
    • 一樣是重複材質,但圖片是鏡像的
  • GL_CLAMP_TO_EDGE
    • 材質座標會被約束在 $[0,1]$,超出的部分會重複邊緣並產生拉伸的效果
  • GL_CLAMP_TO_BORDER
    • 超出的座標為指定的顏色

這些選項都可以對單獨的材質座標軸設定,s, t 和 r 軸(如果是 3D 的話)

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT);
// glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_R, GL_MIRRORED_REPEAT);
  • 選擇 GL_CLAMP_TO_BORDER 則還需要指定一個顏色

紋理(材質)過濾 (Texture Filter)

紋理座標跟解析度無關,所以 OpenGL 要知道怎麼將紋理像素(Texture Pixel, Texel)映射到紋理座標(Texture Coordinate)。
如果一個很大的物體,可是紋理解析度很低,就會有明顯的瑕疵出現。

OpenGL 有許多紋理過濾的選項,但是現在我們只討論重要的兩個:

  • GL_NEAREST(Nearest Neighbor Filtering)

    • OpenGL 默認的過濾方式
    • 選擇中心點最接近紋理座標的那個像素。
  • GL_LINEAR((Bi)linear Filtering)

    • 基於附近的紋理像素,計算出一個插值,近似出這些紋理像素之間的顏色
    • 紋理座標離該紋理像素越接近則對最終顏色貢獻越大

比較遺下兩種方法吧:

GL_NEAREST 產生了顆粒狀的圖案,我們能夠清晰看到組成紋理的像素,而 GL_LINEAR 能夠產生更平滑的圖案,很難看出單個的紋理像素

Mipmapping

假如說場景在開闊的地方,有上千個物體,每個物體上都有紋理,近處的物體的紋理解析度很高,但遠處的物體可能只產生很小的片段(Fragment),在高解析度的紋理中這種物體的顏色通常都不正確,並且也浪費記憶體。

多級漸遠紋理(Mipmap)是一系列的紋理圖像,後一個的大小是前個的二分之一。當觀察者的距離超過一定的閾值(threshold),自動切換至適合該距離的紋理。距離遠用解析度比較小的,也不容易被察覺。

Mipmap 可以自己手動產生,不過 OpenGL 可以幫我們自動產生。

  • 紋理過濾等級 + Mipmap
    • _MIPMAP_NEAREST
      • 使用最接近的 Mip Level
    • _MIPMAP_LINEAR
      • 在兩個最接近的 Mip Level 插值
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

:::info
放大過濾不可以使用 Mipmap ,因為 Mipmap 是給縮小紋理使用的
:::

載入圖片

前面我們已經知道 SFML 可以用來載入圖片了,所以這裡我們就不用其他方法來載入圖片了

// 載入圖片
sf::Image wall;
wall.loadFromFile("../src/1.getting_started/4.1.texture/wall.jpg");
wall.flipVertically();
// 設定材質及參數
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, wall.getSize().x, wall.getSize().y, 0, GL_RGBA, GL_UNSIGNED_BYTE, (const void*)wall.getPixelsPtr());
glGenerateMipmap(GL_TEXTURE_2D);

生成紋理

和之前 OpenGL Texture 一樣,要產生獨一無二的 ID

uint32_t texture;
glGenTexture(1, &texture);

接著需要綁定他

glBindTexture(GL_TEXTURE_2D, texture);

綁定好之後就可以生成紋理了

glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);

完整的流程大概長這樣

unsigned int texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
//
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);   
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
//
sf::Image wall;
wall.loadFromFile("wall.jpg");
wall.flipVertically();
uint32_t width = wall.getSize().x;
uint32_t height = wall.getSize().y;

glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);

使用他

這面我們會使用 glDrawElements 繪製前面 Hello Traingle 中後面教的矩形。我們要告訴 OpenGL 如何使用紋理,所以必須使用紋理座標更新頂點數據。

float vertices[] = {
//     ---- 位置 ----       ---- 颜色 ----     - 纹理坐标 -
     0.5f,  0.5f, 0.0f,   1.0f, 0.0f, 0.0f,   1.0f, 1.0f,   // 右上
     0.5f, -0.5f, 0.0f,   0.0f, 1.0f, 0.0f,   1.0f, 0.0f,   // 右下
    -0.5f, -0.5f, 0.0f,   0.0f, 0.0f, 1.0f,   0.0f, 0.0f,   // 左下
    -0.5f,  0.5f, 0.0f,   1.0f, 1.0f, 0.0f,   0.0f, 1.0f    // 左上
};

一樣因為額外增加頂點屬性,我們必須叫素 OpenGL 新的頂點格式。

uint32_t aPos = glGetAttribLocation(program, "aPos");
glVertexAttribPointer(aPos, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void *)0);
glEnableVertexAttribArray(aPos);
uint32_t aVertColor = glGetAttribLocation(program, "aVertColor");
glVertexAttribPointer(aVertColor, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void *)(3 * sizeof(float)));
glEnableVertexAttribArray(aVertColor);
uint32_t aTexCoord = glGetAttribLocation(program, "aTexCoord");
glVertexAttribPointer(aTexCoord, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6*sizeof(float)));
glEnableVertexAttribArray(aTexCoord);

接著調整 Shader

  • Vertex Shader
#version 450 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aVertColor;
layout (location = 2) in vec2 aTexCoord;

out vec3 VertColor;
out vec2 TexCoord;

void main()
{
    gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
    TexCoord = aTexCoord;
    VertColor = aVertColor;
}
  • Fragment Shader
    • GLSL 中有個類型叫 Sampler
      • Sampler1D, Sampler2D, Sampler3D
#version 450 core
out vec4 FragColor;

in vec2 TexCoord;
in vec3 VertColor;

uniform sampler2D texture1;

uniform vec4 fColor;

void main()
{
    FragColor = texture(texture1, TexCoord) * fColor;
}

GLSL 內建 texture() 函數來採樣,第一個參數是採樣的紋理,第二個參數是紋理座標,輸出去紋理座標(插值後)並且經過 Filter 後的顏色。

之後使用之前記得綁定就好了。

glBindTexture(GL_TEXTURE_2D, texture);
glBindVertexArray(vao);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);

紋理單元 Texture Unit

在 Fragment Shader 中如果想要使用多個紋理,則要給紋理一個位置值,稱作紋理單元(Texture Unit)。

但是在上個範例中並沒有用 glUniform 給值(因為預設是 0)

glActiveTexture(GL_TEXTURE0); // 啟用第 0 號 texture
glBindTexture(GL_TEXTURE_2D, texture); // 將紋理綁定上 GL_TEXTURE0

OpenGL 保證有 16 個紋理單元可以使用,且紋理單元的編號是連續的,意味著可以用 GL_TEXTURE0 + n 來存取(也可以直接用 GL_TEXTUREn)

  • 例子:混和兩張紋理

    • Fragment Shader
    #version 450 core
    
    uniform sampler2D tex1;
    uniform sampler2D tex2;
    
    void main()
    {
        FragColor = mix(texture(tex1, TexCoord), texture(tex2, TexCoord), 0.2);
    }
    
    • mix(a, b, alpha) 混和(線性插值)
      • $\text{color} = a \alpha + b (1-\alpha)$
  • 在開始渲染之前要設定 texture 的編號

    • 如此一來在 fragment shader 裡頭的 tex1, tex2 才會參考到綁定在紋理單元的 Texture
glUseProgram(program);
glUniform1i(glGetUniformLocation(program, "tex1"), 0);
glUniform1i(glGetUniformLocation(program, "tex2"), 1);

渲染流程會變成:

/* Create, Load Texture */
/* ... */
glUseProgram(program);
// Set Texture units
glUniform1i(glGetUniformLocation(program, "tex1"), 0);
glUniform1i(glGetUniformLocation(program, "tex2"), 1);

while(...)
{
    /* ... */

    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D, texture1);
    glActiveTexture(GL_TEXTURE1);
    glBindTexture(GL_TEXTURE_2D, texture2);

    glBindVertexArray(VAO);
    glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
}

那成功的話大概會長這樣

完整 code 請看 這裡

根據時間改變 mix

參考資料

https://learnopengl.com/Getting-started/Textures


上一篇
Day11 [OpenGL] Shader
下一篇
Day 13 [OpenGL] Transformations
系列文
寫遊戲初體驗30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言